package BNS.kingPawn;

import java.io.*;
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
import java.lang.Thread;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
import BNS.graphs.*;
import BNS.plumbing.*;

/** <p>This class implements a version of the King&Pawn game where
tipping is allowed.</p>

<p>When the game reaches a state of maximum social welfate, the game state 
corresponds to a maximum independent set (IS) in the graph.</p>

<p>Payoffs to individual players depend on the roles and tips of others,
and are accumulated continuously during play time.</p>

<p>The counterpart class in the client machine is a MarketISPersona.</p>
*/
public class MarketISGame extends BNSGameController {
	static final int KING=2, PAWN=1;	// These are ROLE codes, not colors!
	static final int pawnInt, kingInt, hurtInt;	// These are color codes.
	static{ pawnInt= BNS.graphs.GraphPanel.intCode("green");
			hurtInt= BNS.graphs.GraphPanel.intCode("red"); 
			kingInt= BNS.graphs.GraphPanel.intCode("violet"); }
	static final float kingPayRate=1.0f, pawnPayRate=0.5f;
	
	protected EdgeState[] state;		// defines entire state of game
	protected NodeState[] nodeStates;	// stuff pertaining to nodes, indexed by Node.getIndex()s
	
//	protected String[] payOffStr;	// formatted String version of payOff[]
//	protected String payoffDefn;	// words shown to player defining the game payoff scheme
	protected int perfection, currentProgress;
	protected static final long serialVersionUID = 01L;
	protected int gameSize;
//	protected StatusMonitor statusMonitor;
	protected StatusPanel statusPanel;
	
	/** Creates a serializable game with all parameters initialized.
		@param gp a GraphPack defining graph views for all players.
		@param time the maximum amount of game time allowed
		@param leanPay the gross pay given to a Pawn node
		@param phatPay the gross pay given to a King node that has no King neighbours 
		*/
	public MarketISGame(GraphPack gp, int time, float leanPay, float phatPay) {
		super(time);
		grpk= gp;
		System.err.println("graph info...\n "+grpk.getInfo());
		int maxISsize= Integer.parseInt(grpk.properties.getProperty("maxISsize","10"));
		//	progressBar shows average payrate,
		//	which will always be a number between 0 and kingPayRate
		perfection= (int)(kingPayRate*1000);
		System.err.println("perfection="+ perfection);
		System.err.println("kingInt="+kingInt+"  pawnInt="+pawnInt);
		}
		
	/** a routine that gets called at run time, to finish setting up
		things that cannot be initialized in the constructor.
		(For instance, the constructor should NOT create any GUI elements.)
		@param mgr	the Session manager, which can be queried for information about 
					the clients, players, etc. that are assigned to this game.
		@param locn the recommended pixel location for the top left corner of the monitor GUI. 
		@param sessionLog the place where metadata is recorded
		@param gameLog the place where game actions are recorded
		*/	
	public void init(Manager mgr, Point locn, PrintStream sessionLog, PrintStream gameLog) {
		super.init(mgr, locn, sessionLog, gameLog);
		sessionLog.println(getTitle()+"  "+ new Date());
		sessionLog.println("source "+ getSource());
		sessionLog.println(grpk.getInfo());
		gameSize= grpk.graph.numNodes();
		gameValue= "unsolved";
		int numEdges= grpk.graph.numEdges();
		state= new EdgeState[numEdges];
		int eNum=0;
		Enumeration ene= grpk.graph.enumEdges();
		while (ene.hasMoreElements()) {
			Edge e= (Edge)ene.nextElement();
			state[eNum++]= new EdgeState(e.from.getIndex(),e.to.getIndex(),PAWN);
			}
		assert(eNum==numEdges);
//		System.err.println("graph state: "+state);
//		for (int i=0; i<state.length; i++) System.err.println(state[i]);
		
//		System.err.println("listing all "+gameSize+" sub-states...");
		nodeStates= new NodeState[gameSize];
		for (int nsi=0; nsi<nodeStates.length; nsi++) {
			NodeState nodeState= nodeStates[nsi]= new NodeState(nsi);
			grpk.graph.getNode(nsi).color= pawnInt;
			Graph graphlet= grpk.graphlets[nsi];
			nodeState.edgeStates= new EdgeState[graphlet.numNodes()-1];
			ene= graphlet.enumEdges();
			int eNumlet= 0;
			while (ene.hasMoreElements()) {
				Edge e= (Edge)ene.nextElement();
				// discard any edges not connected to eigenNode
				if (!e.contains(nsi)) continue;
				eNum= 0;
				while (!e.joins(state[eNum].frIdx,state[eNum].toIdx)) eNum++;
				nodeState.edgeStates[eNumlet++]= state[eNum];
				}
			assert (eNumlet==nodeState.edgeStates.length);
//			System.err.println("subgraph "+nsi+" state: "+nodeState.edgeStates);
//			for (int i=0; i<nodeState.edgeStates.length; i++) 
//				System.err.println(nodeState.edgeStates[i]);
//			nodeState.fights= -1;	// means not fighting anyone
			nodeState.tFrac= 0.f;
			nodeState.role= PAWN;
			}		
		ShowTipsPanel gPanel= new ShowTipsPanel(nodeStates,state);
//		gPanel.setEdgeStates(state);
		monitor= new Monitor(gPanel,locn);
		gPanel.init(grpk.graph);
//		statusPanel= new StatusPanel(timeLimit,perfection,true);
//		statusMonitor= new StatusMonitor(statusPanel,locn);
		calcWinStatus(0); calcPayRates(0);
		currentProgress= (int)(pawnPayRate*1000);
		}
	
	public int monitorUpdate(Monitor monitor) {
		monitor.welfareDisplay.setText(String.format(" $%.2f/min ",currentProgress/1000.));
		monitor.timeDisplay.setText(String.format(" %d ",clock.getElapsedTime()/1000));
		return 460;
		}
	
	public int getSize() { return grpk.size(); }
	
	public String getTitle() { return "MarketISGame"; }
	
	public String getInfo() { 
		return getTitle() +" of size "+ getSize() +"\n"
//			+" biggest possible Independent Set= "+ perfection +"\n"
//			+"\n graphpack info= "+ grpk.getInfo()
			+" time allowed="+ timeLimit;
		}
	
	/** used to change the state of a game on behalf of a client during a game replay.
		The first char of tokens[4] determines where this request gets routed next. 
		It must be either 'C' or 'T'; if 'C' then this call is interpreted as a 
		change of color; if 'T' then it is interpreted as a change of tipping rate.
		<p>It returns true iff tokens[4] starts with 'C' or 'T', the game is not over, 
		the game is not paused, and the game had no errors while trying to carry out the changes. 
		@param client representing the player making the change.
		@param tokens array of String holding all arguments required for the change.
		@return indicator of success at making the change. 
		*/
	public boolean interpret(long tstamp, ClientLiaison client, String[] tokens) {
		char type= tokens[4].charAt(0);
		System.err.println(" did "+type+" "+tokens[5]);
		System.err.print(".");		// TODO:					change this ==2 to ==KING
		if (type=='C') return changeColor(tstamp,client,Integer.parseInt(tokens[5])==2? 2:1);
		if (type=='T') return changeTip(tstamp,client,new Double(tokens[5]));
		else System.err.println("type="+type+"??");
		return false;
		}
	
	/** invoked (indirectly) by a Client to change the state of a game.
		The classtype of the changeObject determines where this request gets routed next. 
		It must be either an Integer or a Double; if Integer then this call is interpreted as a 
		change of color; if Double then it is interpreted as a change of tipping rate.
		<p>It returns true iff the game is not over, the game is not paused, 
		and the game had no errors while trying to carry out the changes implied by changeObject. 
		@param client representing the player making the change.
		@param changeObj indicating what type of change it is, as well as the value involved.
		@return indicator of success at making the change.
		*/
	public boolean alter(ClientLiaison client, Serializable changeObj) {
		long tstamp= clock.getElapsedTime();
		if (changeObj instanceof Integer) return changeColor(tstamp,client,(Integer)changeObj);
		if (changeObj instanceof Double ) return changeTip  (tstamp,client,(Double) changeObj);
		return false;
		}
	
	/** The method that implements player action of selecting a color.
		It returns true if the game is not over, the game is not paused, and the color is either 
		KING or PAWN. Otherwise it returns false to let the Persona know that its action failed.
		@param client identifies the Client (and perforce the node and the player).
		@param roleButton indicates which color is selected. 
		*/
	protected boolean changeColor(long tstamp, ClientLiaison client, Integer roleButton) {
		//System.err.println(" client #"+client+" changing  to "+value);
		int role= roleButton.intValue();
		//System.err.println(" event #"+ nEvents+": node "+ node +" changing  to "+role);
		//System.err.println(" (game pause "+clock.suspended()+", complete "+gameCompleted +")");
		if (client==null) System.err.println(" client==null !");
		if (client.eigenNode==null) System.err.println(" eigenNode==null !");
		int nodeIdx= client.eigenNode.getIndex();
		synchronized(state) {	// only this section needs to be threadsafe
			if (clock.suspended() || gameCompleted) return false;
//			nEvents++;
			gameLog.println("move "+tstamp+" "+client.clientID+" "+client.eigenNode+" C "+role);
			//System.err.println(" node #"+ nodeName +" changed  to "+role);
//			int countRYedges= 0;
			for (int i=0; i<gameSize; i++) {
				nodeStates[i].fights= -1;	// -1 means no fights
				}
			for (int i=0; i<state.length; i++) {
				EdgeState es= state[i];
				if (es.frIdx==nodeIdx) es.frRol= role;
				if (es.toIdx==nodeIdx) es.toRol= role;
//				if (es.frRol==KING ^ es.toRol==KING) countRYedges++;
				if (es.frRol==KING)
					if (nodeStates[es.toIdx].fights> -1) nodeStates[es.toIdx].fights= -2;
					else nodeStates[es.toIdx].fights= es.frIdx;	// indicates who he would fight
				if (es.toRol==KING) 
					if (nodeStates[es.frIdx].fights> -1) nodeStates[es.frIdx].fights= -2;
					else nodeStates[es.frIdx].fights= es.toIdx;	// indicates who he would fight 
				}
			nodeStates[nodeIdx].role= role;
			calcWinStatus(tstamp);
			calcPayRates(tstamp);
			grpk.graph.getNode(nodeIdx).color= role==KING? kingInt:pawnInt;
			if (role==KING && nodeStates[nodeIdx].fights != -1)
				grpk.graph.getNode(nodeIdx).color= hurtInt;
			}
		monitor.repaint();
//		statusMonitor.timerUpdate(tstamp);
//		statusMonitor.progressUpdate(currentProgress);
		return true;
		}
	
	/** This routine goes through the whole network and calculates everyone's current
		conflicts are; this depends on their own roles and their neighbours' roles.
		The results are stored in nodeStates[ ].winStatus: 1 for pawn, 2 for king, 0 for fight.
		This routine also updates the overall social welfare and elapsed time displays
		in the Monitor.
		*/
	private void calcWinStatus(long tstamp) {
		int countWins= 0;
//		for (int i=0; i<gameSize; i++) System.err.print(" "+nodeStates[i].fights);
//		System.err.println("  <== fights");
		gameLog.print("stat");
		for (int i=0; i<gameSize; i++) { 
			nodeStates[i].winStatus= (nodeStates[i].role==KING)? 
									(nodeStates[i].fights== -1? 2 : 0) : 1;
			gameLog.print(" "+nodeStates[i].winStatus);
			countWins+= nodeStates[i].winStatus;
			//System.err.print(" "+nodeStates[i].winStatus);
			}
		currentProgress= (int)(countWins*pawnPayRate*1000/gameSize);
		gameLog.println(" "+currentProgress/1000.);
		try {	// this is somewhat redundant with monitorUpdate() of the Monitor thread:
			monitor.welfareDisplay.setText(String.format(" $%.2f/min ",currentProgress/1000.));
			monitor.timeDisplay.setText(String.format(" %d ",tstamp/1000));
		} catch(Exception e) {
			System.err.println(e);
			System.err.println("monitor="+monitor);
			System.err.println("monitor.welfareDisplay="+monitor.welfareDisplay);
			System.err.println("monitor.timeDisplay="+monitor.timeDisplay);
			}
//System.err.println(countWins+"=winStatus,  currentProgress= "+currentProgress+"/"+perfection);
		}
	
	/** This routine goes through the whole network and calculates what everyone should be getting
		paid; this depends on their winStatus, and the current tipping rates.
		Thus, calcWinStatus() should be called just before calling this routine.
		*/
	private void calcPayRates(long tstamp) {
		long thisPayDate= tstamp;
		//for (int i=0; i<gameSize; i++) System.err.format("  %4d", nodeStates[i].fights);
		//System.err.println("  <== fights");
		for (int nn=0; nn<gameSize; nn++) {
			updateCumPay(thisPayDate,nn);
			nodeStates[nn].kRate= (nodeStates[nn].fights== -1? kingPayRate:0);
			nodeStates[nn].pRate= pawnPayRate;
			}
		for (int i=0; i<state.length; i++) {
			EdgeState es= state[i];
			int nfr= es.frIdx;
			int nto= es.toIdx;
			if (nodeStates[nfr].winStatus==2 && nodeStates[nfr].fights== -1) { 
				nodeStates[nfr].kRate-= es.frTip; 
				nodeStates[nto].pRate+= es.frTip; }
			if (nodeStates[nto].winStatus==2 && nodeStates[nto].fights== -1) { 
				nodeStates[nto].kRate-= es.toTip; 
				nodeStates[nfr].pRate+= es.toTip; }
			
			if (nodeStates[nfr].role==KING 
			&& nodeStates[nto].fights==nfr
			&& nodeStates[nfr].fights==nto) 
				nodeStates[nfr].pRate+= es.toTip; 
			if (nodeStates[nto].role==KING 
			&& nodeStates[nfr].fights==nto
			&& nodeStates[nto].fights==nfr) 
				nodeStates[nto].pRate+= es.frTip; 
			}
		gameLog.print("rate");
		for (int nn=0; nn<gameSize; nn++) {
			if (nodeStates[nn].winStatus==1 && nodeStates[nn].fights==-1) 
				nodeStates[nn].kRate= kingPayRate * (1.0f-nodeStates[nn].tFrac);
			nodeStates[nn].kRate= Math.max(nodeStates[nn].kRate,0);
			nodeStates[nn].payRate= (nodeStates[nn].role==KING)? 
									nodeStates[nn].kRate : nodeStates[nn].pRate;
			gameLog.print(" "+nodeStates[nn].payRate);
			}
		gameLog.println();
//		for (int i=0; i<gameSize; i++) System.err.format("  %.2f", nodeStates[i].payRate);
//		System.err.println("  <== payRates");
		}
	
	/** Alters the side payment values.
		A side payment is a pay rate paid by the tipping node to its neighbours, but only while
		all the neighbours are Pawns. Whenever some neighbour is a King, no side payment occurs.	
		@param client the client making the call, 
			which also identifies the node (and player) requesting this change.
		@param tipObj	the value in dollars per timeunit the client is willing to pay other clients
		@return a flag indicating if the change was registered. 
		*/
	public boolean changeTip(long tstamp, ClientLiaison client, Double tipObj) {
		// TODO: make the tipObj into a primitive double ?
		float tipFrac= (float)tipObj.doubleValue();
		int nodeIdx= client.eigenNode.getIndex();
		//System.err.println(" new tip="+ tipFrac);
		if (tipFrac>1.0) {
			System.err.println(tstamp+" clientID "+client.clientID+" tip too high "+tipFrac);
			return false;
			}
		if (tipFrac<0.0) {
			System.err.println(tstamp+" clientID "+client.clientID+" tip too low "+tipFrac);
			return false;
			}
		//System.err.println(" (game pause "+clock.suspended()+", complete "+gameCompleted +")");
		synchronized(state) {	// only this section needs to be threadsafe
			if (clock.suspended() || gameCompleted) return false;
			gameLog.println("move "+tstamp+" "+client.clientID+" "+client.eigenNode
							+" T "+tipFrac);
			nodeStates[nodeIdx].tFrac= tipFrac;
			int nNbrs= grpk.graphlets[nodeIdx].numNodes()-1;
			for (int i=0; i<state.length; i++) {
				EdgeState es= state[i];
				if (es.frIdx==nodeIdx) es.frTip= kingPayRate*tipFrac/nNbrs; 
				if (es.toIdx==nodeIdx) es.toTip= kingPayRate*tipFrac/nNbrs; 
				}
			}
		calcPayRates(tstamp);
		monitor.repaint();
		return true;
		}
	
	/** updates the accumulated pay, and notes when this happened.
		Since the payrate changes only at discrete events, the pay over the most recent interval 
		is found by multiplying the current payrate by the length of the interval.
		This routine must be called whenever the rate changes! 
		otherwise the product will be erroneous.
		*/ 
	private float updateCumPay(long thisPayDate, int nodeIdx) {
		NodeState nState= nodeStates[nodeIdx];
		nState.cumulativePay+= nState.payRate * (thisPayDate-nState.lastPayDate)/(1000*60);
		nState.lastPayDate= thisPayDate;
		return nState.cumulativePay;
		}
	
	/** called by ClientLiaison on behalf of Client to get data relevant to a node. 
		@param nodeID the identifying index of the node in the game graph.
		@return an Envelope, which MarketISPersona must interpret as the
		tips and colors and payRates of each of the nodes in its subgraph. 
		*/
	public ClientLiaison.StatusCarrier getUpdate(int nodeID) { // TODO: synchronized
		Envelope env= new Envelope();
		env.elapsedTime= clock.getElapsedTime();
		env.currentProgress= currentProgress;		
		updateCumPay(env.elapsedTime, nodeID);
		// The clientStates datastructure is fixed, but the data in it is updated by calls 
		// to changeTips and changeColor. Here we simply return the array that holds the data.
		env.local= nodeStates[nodeID];
		return env;
		}
	
	public BNSPersona getPersona(ClientLiaison client, Node node) {
		System.err.println("creating new MarketISPersona("+ node +") for "+ client.clientID);
		int nodeIdx= node.getIndex();
		nodeStates[nodeIdx].client= client;
		return new MarketISPersona(client, grpk.graphlets[nodeIdx],
					gameSize, timeLimit, perfection);
		}
	
	public float getPayoff(int nodeID, int time) {
		return updateCumPay(time, nodeID); }
	
	/** Goes through all clients, calculates their final payoff, and sends a message
		to each client reporting same.	*/
	public void showPayoffs(int time) {
		float gamePay= 0.0f;
		gameLog.print("earn");
		for (int nsi= 0; nsi<nodeStates.length; nsi++) {
			ClientLiaison client= nodeStates[nsi].client;
//			if (client==null || client.game!=gameController || client.empty) continue;
			float pay= getPayoff(client.eigenNode.getIndex(), time);
			gamePay+= pay;
			client.sendMessage(String.format("game OVER; your final payoff is $%.2f", pay));
			gameLog.format(" %f",pay);
			}
		gameLog.format(" %f%n",gamePay);
//		gameLog.print("clid");
//		for (int nsi=0; nsi<nodeStates.length; nsi++) 
//			gameLog.print(" "+nodeStates[nsi].client.clientID);
//		gameLog.println();
		}
	
	} // EOC MarketISGame
